# encoding: utf-8
"""
@desc: 基于物品的协同过滤算法ItemCF
      (1) 计算物品之间的相似度。
      (2) 根据物品的相似度和用户的历史行为给用户生成推荐列表。
@author: rannuo
@contact: rannuo1010@gmail.com
@time: 2019/4/17 11:04
@file: ItemCF.py
"""

from numpy import *


def item_similarity(train):
    """
    计算物品相似度矩阵
    :param train: 训练集
    :return: 物品相似度矩阵W,W[u][v]表示u,v的相似度
    :return: 相关用户user_related_users字典
    """
    N = dict()  # |N(i)|是喜欢物品i的用户数
    C = dict()  # N(i)∩N(j)同时喜欢物品i和物品j的用户数
    for _, item in train.items():
        for i in item.keys():
            N.setdefault(i, 0)
            N[i] += 1
            for j in item.keys():
                if i == j:
                    continue
                C.setdefault(i, {})
                C[i].setdefault(j, 0)
                C[i][j] += 1
                # C[i][j] += 1 / math.log(1 + len(item) * 1.0) # ItemCF-IUF算法

    # 计算物品相似度矩阵
    W = dict()
    for i, related_items in C.items():
        W.setdefault(i, dict())
        for j, cij in related_items.items():
            W[i].setdefault(j, 0)
            W[i][j] = cij / math.sqrt(N[i] * N[j] * 1.0)
    return W


def recommend(user, train, N, K, W):
    rank = dict()
    ru = train.get(user, {})
    for i, pi in ru.items():
        for j, wj in sorted(W[i].items(), key=lambda x: x[1], reverse=True)[0:K]:
            if j in ru:
                continue
            rank.setdefault(j, 0)
            # rank[j] += pi * wj
            rank[j] += pi
    return dict(sorted(rank.items(), key=lambda x: x[1], reverse=True)[0:N])


def calc_recall(train, test, N, k, W):
    hit = 0
    total = 0
    for user in train.keys():
        tu = test.get(user, {})
        rank = recommend(user, train, N, k, W)
        for item, _ in rank.items():
            if item in tu:
                hit += 1
        total += len(tu)
    return hit / (total * 1.0)


def calc_precision(train, test, N, k, W):
    hit = 0
    total = 0
    for user in train.keys():
        tu = test.get(user, {})
        rank = recommend(user, train, N, k, W)
        for item, _ in rank.items():
            if item in tu:
                hit += 1
        total += N
    return hit / (total * 1.0)


def calc_coverage(train, N, K, W):
    recommend_items = set()
    all_items = set()
    for user in train.keys():
        for item in train[user].keys():
            all_items.add(item)
        rank = recommend(user, train, N, K, W)
        for item, _ in rank.items():
            recommend_items.add(item)
    return len(recommend_items) / (len(all_items) * 1.0)


def calc_popularity(train, N, K, W):
    item_popularity = dict()
    for user, items in train.items():
        for item in items.keys():
            if item not in item_popularity:
                item_popularity.setdefault(item, 0)
            item_popularity[item] += 1
    ret = 0
    n = 0
    for user in train.keys():
        rank = recommend(user, train, N, K, W)
        for item, _ in rank.items():
            ret += math.log(1 + item_popularity[item])
            n += 1
    return ret / (n * 1.0)


def calc_accuracy(train, test, N, k, W):
    hit = 0
    recall_total = 0
    precision_total = 0
    for user in train.keys():
        tu = test.get(user, {})
        rank = recommend(user, train, N, k, W)
        for item, _ in rank.items():
            if item in tu:
                hit += 1
        recall_total += len(tu)
        precision_total += N
    recall = hit / recall_total * 1.0
    precision = hit / precision_total * 1.0
    # return 2 * recall * precision / (recall + precision)
    return precision / recall


def calc_evaluation_index(train, test, N, K, W):
    """
    计算评测指标
    :param train: 训练集
    :param test: 测试集
    :param N: 推荐列表长度(Top-N)
    :param K: 邻接数
    :param W: 相似矩阵
    :return: 召回率、准确率、覆盖率、平均流行度、算法精度
    """
    hit = 0
    tu_total = 0
    ru_total = 0
    popularity_ret = 0
    recommend_items = set()
    all_items = set()
    item_popularity = dict()

    for user, items in train.items():
        for item in items.keys():
            if item not in item_popularity:
                item_popularity.setdefault(item, 0)
            item_popularity[item] += 1

    for user in train.keys():
        for item in train[user].keys():
            all_items.add(item)
        # 用户u在测试集上喜欢的物品集合为T(u)
        tu = test.get(user, {})
        # 对用户u推荐N个物品（记为R(u)）
        ru = recommend(user, train, N, K, W)
        for item, _ in ru.items():
            if item in tu:
                hit += 1
            popularity_ret += math.log(1 + item_popularity[item])  # 流行度分布满足长尾分布，在取对数后，流行度的平均值更加稳定
            recommend_items.add(item)
        tu_total += len(tu)
        ru_total += N

    recall = hit / tu_total * 1.0  # 计算召回率
    precision = hit / ru_total * 1.0  # 计算准确率
    coverage = len(recommend_items) / len(all_items) * 1.0  # 计算覆盖率
    popularity = popularity_ret / ru_total  # 计算流行度
    accuracy = precision / recall  # 计算精确度
    return recall, precision, coverage, popularity, accuracy
